package org.skydiveapplibrary.util;

import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.skydiveapplibrary.data.Aircraft;
import org.skydiveapplibrary.data.AltitudeUnit;
import org.skydiveapplibrary.data.Database;
import org.skydiveapplibrary.data.FreefallProfileType;
import org.skydiveapplibrary.data.Location;
import org.skydiveapplibrary.data.LogEntry;
import org.skydiveapplibrary.data.LogbookHistory;
import org.skydiveapplibrary.data.Rig;
import org.skydiveapplibrary.data.RigComponent;
import org.skydiveapplibrary.data.RigReminder;
import org.skydiveapplibrary.data.Signature;
import org.skydiveapplibrary.data.SkydiveType;
import org.skydiveapplibrary.data.TimeUnit;
import org.skydiveapplibrary.repository.AircraftRepository;
import org.skydiveapplibrary.repository.LocationRepository;
import org.skydiveapplibrary.repository.LogEntryRepository;
import org.skydiveapplibrary.repository.LogbookHistoryRepository;
import org.skydiveapplibrary.repository.RigRepository;
import org.skydiveapplibrary.repository.SignatureRepository;
import org.skydiveapplibrary.repository.SkydiveTypeRepository;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.content.Context;
import android.util.Base64;

public class DatabaseImportUtil
{
	// context
	private Context context;
	
	// date format
	private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
	private DateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
	
	// maps
	private Map<String, SkydiveType> skydiveTypeMap = new HashMap<String, SkydiveType>();
	private Map<String, Location> locationMap = new HashMap<String, Location>();
	private Map<String, Aircraft> aircraftMap = new HashMap<String, Aircraft>();
	private Map<String, Rig> rigMap = new HashMap<String, Rig>();
	
	// import types
	private enum ImportType
	{
		None,
		New,
		Update
	}
	
	public DatabaseImportUtil(Context context)
	{
		this.context = context;
	}
	
	public void writeXmlToDB(File xmlFile)
		throws IOException, ParserConfigurationException, SAXException
	{
		DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		Document document = builder.parse(xmlFile);

		// set timezone of format
		dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
		
		// get main elements
		Element logbookElement = document.getDocumentElement();

		// check XML version
		int xmlVersion = getIntegerAttributeValue(logbookElement, "version", 0);
		if (xmlVersion < 2)
			throw new IOException("SkydivingLogbook XML version not supported");
		
		Element historyNode = findElement(logbookElement, "logbook_history");
		Element logEntriesNode = findElement(logbookElement, "log_entries");
		Element signaturesNode = findElement(logbookElement, "signatures");
		Element skydiveTypesNode = findElement(logbookElement, "skydive_types");
		Element locationsNode = findElement(logbookElement, "locations");
		Element aircraftsNode = findElement(logbookElement, "aircrafts");
		Element rigsNode = findElement(logbookElement, "rigs");
		
		// update history
		updateLogbookHistory(historyNode);
		
		// add skydive types
		addSkydiveTypes(skydiveTypesNode);
		
		// add locations
		addLocations(locationsNode);
		
		// add aircrafts
		addAircrafts(aircraftsNode);

		// add rigs
		addRigs(rigsNode);

		// add log entries
		addLogEntries(logEntriesNode, signaturesNode);
	}
	
	private void updateLogbookHistory(Element historyNode)
	{
		LogbookHistoryRepository repository = new LogbookHistoryRepository(context);
		// update history
		LogbookHistory history = new LogbookHistory();
		history.setFreefallTime(getIntegerValue(historyNode, "freefall_time", 0));
		history.setCutaways(getIntegerValue(historyNode, "cutaways", 0));
		// save
		repository.updateHistory(history);
	}
	
	private void addSkydiveTypes(Element skydiveTypesNode)
	{
		// skydive type repo
		SkydiveTypeRepository repository = new SkydiveTypeRepository(context);
		
		// get all skydive types
		NodeList skydiveTypeNodes = skydiveTypesNode.getElementsByTagName("skydive_type");
		for (int i = 0; i < skydiveTypeNodes.getLength(); i++)
		{	
			// get skydive type
			Element skydiveTypeNode = (Element)skydiveTypeNodes.item(i);
			
			// get field nodes
			String id = getStringValue(skydiveTypeNode, "id");
			String name = getStringValue(skydiveTypeNode, "name");
			Date lastModifiedDate = getDateTimeValue(skydiveTypeNode, "last_modified_utc");
			
			SkydiveType skydiveType = null;
			ImportType importType = ImportType.None;
			
			if (id.length() > 0 && repository.exists(Database.SKYDIVE_TYPE_UNIQUE_ID, id))
			{
				skydiveType = repository.getFromUniqueId(id);
				
				if (lastModifiedDate.compareTo(skydiveType.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else if (repository.exists(Database.SKYDIVE_TYPE_NAME, name))
			{
				skydiveType = repository.findSkydiveType(name);
				
				if (lastModifiedDate.compareTo(skydiveType.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else
			{
				importType = ImportType.New;
				
				// create new
				skydiveType = new SkydiveType();
				skydiveType.setName(name);
			}
					
			if (importType != ImportType.None)
			{
				skydiveType.setName(name);
				skydiveType.setActive(getBooleanValue(skydiveTypeNode, "active", false));
				skydiveType.setNotes(getStringValue(skydiveTypeNode, "notes"));
				skydiveType.setLastModifiedUtc(lastModifiedDate.equals(new Date(0)) ? new Date(System.currentTimeMillis()) : lastModifiedDate);
				String freefallProfile = getStringValue(skydiveTypeNode, "freefall_profile");
				skydiveType.setUniqueId(id);
				
				if (freefallProfile != null && freefallProfile.length() > 0)
					skydiveType.setFreefallProfileType(FreefallProfileType.valueOf(freefallProfile));

				// save
				if (importType == ImportType.New)
				{
					// add new
					repository.addSkydiveType(skydiveType);
				}
				else if (importType == ImportType.Update)
				{
					// save update
					repository.updateSkydiveType(skydiveType, false);
				}
			}
			
			// add to map
			skydiveTypeMap.put(id, skydiveType);
		}
	}
	
	private void addLocations(Element locationsNode)
	{
		// location repo
		LocationRepository repository = new LocationRepository(context);
		
		// get all locations
		NodeList locationNodes = locationsNode.getElementsByTagName("location");
		for (int i = 0; i < locationNodes.getLength(); i++)
		{	
			// get location
			Element locationNode = (Element)locationNodes.item(i);
			
			// get field nodes
			String id = getStringValue(locationNode, "id");
			String name = getStringValue(locationNode, "name");
			Date lastModifiedDate = getDateTimeValue(locationNode, "last_modified_utc");
			
			Location location = null;
			ImportType importType = ImportType.None;
			
			if (id.length() > 0 && repository.exists(Database.LOCATION_UNIQUE_ID, id))
			{
				location = repository.getFromUniqueId(id);
				
				if (lastModifiedDate.compareTo(location.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else if (repository.exists(Database.LOCATION_NAME, name))
			{
				location = repository.findLocation(name);
				
				if (lastModifiedDate.compareTo(location.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else
			{
				importType = ImportType.New;
				
				// create new
				location = new Location();
				location.setName(name);
			}
			
			if (importType != ImportType.None)
			{
				location.setName(name);
				location.setActive(getBooleanValue(locationNode, "active", false));
				location.setHome(getBooleanValue(locationNode, "home", false));
				location.setNotes(getStringValue(locationNode, "notes"));
				location.setLastModifiedUtc(lastModifiedDate.equals(new Date(0)) ? new Date(System.currentTimeMillis()) : lastModifiedDate);
				location.setUniqueId(id);
				
				// save
				if (importType == ImportType.New)
				{
					// add new
					repository.addLocation(location);
				}
				else if (importType == ImportType.Update)
				{
					// save update
					repository.updateLocation(location, false);
				}
			}
			
			// add to map
			locationMap.put(id, location);
		}
	}
	
	private void addAircrafts(Element aircraftsNode)
	{
		// aircraft repo
		AircraftRepository repository = new AircraftRepository(context);
		
		// get all aircrafts
		NodeList aircraftNodes = aircraftsNode.getElementsByTagName("aircraft");
		for (int i = 0; i < aircraftNodes.getLength(); i++)
		{	
			// get aircraft
			Element aircraftNode = (Element)aircraftNodes.item(i);
			
			// get field nodes
			String id = getStringValue(aircraftNode, "id");
			String name = getStringValue(aircraftNode, "name");
			Date lastModifiedDate = getDateTimeValue(aircraftNode, "last_modified_utc");
			
			Aircraft aircraft = null;
			ImportType importType = ImportType.None;			
			
			if (id.length() > 0 && repository.exists(Database.AIRCRAFT_UNIQUE_ID, id))
			{
				aircraft = repository.getFromUniqueId(id);
				
				if (lastModifiedDate.compareTo(aircraft.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else if (repository.exists(Database.AIRCRAFT_NAME, name))
			{
				aircraft = repository.findAircraft(name);
				
				if (lastModifiedDate.compareTo(aircraft.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else
			{
				importType = ImportType.New;
				// create new
				aircraft = new Aircraft();
				aircraft.setName(name);
			}
			
			if (importType != ImportType.None)
			{
				aircraft.setName(name);
				aircraft.setActive(getBooleanValue(aircraftNode, "active", false));
				aircraft.setNotes(getStringValue(aircraftNode, "notes"));
				aircraft.setLastModifiedUtc(lastModifiedDate.equals(new Date(0)) ? new Date(System.currentTimeMillis()) : lastModifiedDate);
				aircraft.setUniqueId(id);
				
				if (importType == ImportType.New)
				{
					// add new
					repository.addAircraft(aircraft);
				}
				else if (importType == ImportType.Update)
				{
					// save update
					repository.updateAircraft(aircraft, false);
				}
			}
			
			// add to map
			aircraftMap.put(id, aircraft);
		}
	}
	
	private void addRigs(Element rigsNode)
	{
		// rig repo
		RigRepository repository = new RigRepository(context);
		
		// get all rigs
		NodeList rigNodes = rigsNode.getElementsByTagName("rig");
		for (int i = 0; i < rigNodes.getLength(); i++)
		{	
			// get rig
			Element rigNode = (Element)rigNodes.item(i);
			
			// get field nodes
			String id = getStringValue(rigNode, "id");
			String name = getStringValue(rigNode, "name");
			Date lastModifiedDate = getDateTimeValue(rigNode, "last_modified_utc");
			
			// get rig
			Rig rig = null;
			ImportType importType = ImportType.None;
			
			if (id.length() > 0 && repository.exists(Database.RIG_UNIQUE_ID, id))
			{
				rig = repository.getFromUniqueId(id);
				
				if (lastModifiedDate.compareTo(rig.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else if (repository.exists(Database.RIG_NAME, name))
			{
				rig = repository.findRig(name);
				
				if (lastModifiedDate.compareTo(rig.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else
			{
				importType = ImportType.New;
				// create new
				rig = new Rig();
				rig.setName(name);
			}

			if (importType != ImportType.None)
			{
				rig.setName(name);
				rig.setActive(getBooleanValue(rigNode, "active", false));
				rig.setPrimary(getBooleanValue(rigNode, "primary", false));
				rig.setArchived(getBooleanValue(rigNode, "archived", false));
				rig.setNotes(getStringValue(rigNode, "notes"));
				rig.setLastModifiedUtc(lastModifiedDate.equals(new Date(0)) ? new Date(System.currentTimeMillis()) : lastModifiedDate);
				rig.setUniqueId(id);
				
				// components
				if (importType == ImportType.Update)
					rig.getComponents().clear();
				
				Element componentsNode = findElement(rigNode, "components");
				NodeList componentNodes = componentsNode.getElementsByTagName("component");
				for (int c = 0; c < componentNodes.getLength(); c++)
				{
					Element componentNode = (Element)componentNodes.item(c);
					RigComponent component = new RigComponent();
					component.setName(getStringValue(componentNode, "name"));
					component.setSerialNumber(getStringValue(componentNode, "serial_number"));
					component.setNotes(getStringValue(componentNode, "notes"));
					rig.getComponents().add(component);
				}
				
				// reminders
				if (importType == ImportType.Update)
					rig.getReminders().clear();
				
				Element remindersNode = findElement(rigNode, "reminders");
				NodeList reminderNodes = remindersNode.getElementsByTagName("reminder");
				for (int r = 0; r < reminderNodes.getLength(); r++)
				{
					Element reminderNode = (Element)reminderNodes.item(r);
					RigReminder reminder = new RigReminder();
					reminder.setName(getStringValue(reminderNode, "name"));
					reminder.setInterval(getIntegerValue(reminderNode, "interval", 0));
					reminder.setIntervalUnit(TimeUnit.valueOf(getStringValue(reminderNode, "interval_unit")));
					reminder.setLastCompletedDate(getDateValue(reminderNode, "last_completed_date"));
					rig.getReminders().add(reminder);
				}
				
				if (importType == ImportType.New)
				{
					// add new
					repository.addRig(rig);
				}
				else if (importType == ImportType.Update)
				{
					// save update
					repository.updateRig(rig, false);
				}
			}
			
			// add to map
			rigMap.put(id, rig);
		}
	}
	
	private void addLogEntries(Element logEntriesNode, Element signaturesNode)
	{
		// create signature maps
		Map<String, Signature> signatureMap = new HashMap<String, Signature>();
		Map<String, List<Integer>> signatureLogEntryIds = new HashMap<String, List<Integer>>();
		NodeList signatureNodes = signaturesNode.getElementsByTagName("signature");
		for (int i = 0; i < signatureNodes.getLength(); i++)
		{
			Element signatureNode = (Element)signatureNodes.item(i);
			// get id
			String id = getStringValue(signatureNode, "id");
			// create signature
			Signature signature = new Signature();
			String license = getStringValue(signatureNode, "license");
			signature.setLicense(license == null ? "" : license);
			signature.setImage(getBinaryValue(signatureNode, "image"));
			// add to maps
			signatureMap.put(id, signature);
			signatureLogEntryIds.put(id, new ArrayList<Integer>());
		}
		
		// repo
		LogEntryRepository repository = new LogEntryRepository(context);
		
		// get all entries
		NodeList logEntryNodes = logEntriesNode.getElementsByTagName("log_entry");
		for (int i = 0; i < logEntryNodes.getLength(); i++)
		{
			Element logEntryNode = (Element)logEntryNodes.item(i);
			
			// get values
			String uniqueId = getStringValue(logEntryNode, "id");
			int jumpNumber = getIntegerValue(logEntryNode, "jump_number", -1);
			Date lastModifiedDate = getDateTimeValue(logEntryNode, "last_modified_utc");
	        
			LogEntry entry = null;
			Integer logEntryId = 0;
			ImportType importType = ImportType.None;
			
			if (uniqueId != "" && repository.uniqueIdExists(uniqueId))
			{
				logEntryId = repository.getIdFromUniqueId(uniqueId);
				entry = repository.getLogEntry(logEntryId);
				
				if (lastModifiedDate.compareTo(entry.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else if (repository.exists(jumpNumber))
			{
				logEntryId = repository.getIdFromJumpNumber(jumpNumber);
				entry = repository.getLogEntry(logEntryId);
				
				if (lastModifiedDate.compareTo(entry.getLastModifiedUtc()) > 0)
				{
					importType = ImportType.Update;
				}
			}
			else
			{
				importType = ImportType.New;
				
				// create new
				entry = new LogEntry();
			}
			
			if (importType != ImportType.None)
			{
				// update fields
				entry.setJumpNumber(jumpNumber);
				entry.setDate(getDateValue(logEntryNode, "date"));
				entry.setSkydiveType(skydiveTypeMap.get(getStringValue(logEntryNode, "skydive_type_id")));
				entry.setAircraft(aircraftMap.get(getStringValue(logEntryNode, "aircraft_id")));
				entry.setLocation(locationMap.get(getStringValue(logEntryNode, "location_id")));
				entry.setAltitudeUnit(AltitudeUnit.valueOf(getStringValue(logEntryNode, "altitude_unit")));
				entry.setCutaway(getBooleanValue(logEntryNode, "cutaway", false));
				entry.setDeploymentAltitude(getIntegerValue(logEntryNode, "deployment_altitude", 0));
				entry.setExitAltitude(getIntegerValue(logEntryNode, "exit_altitude", 0));
				entry.setFreeFallTimeSeconds(getIntegerValue(logEntryNode, "freefall_time", 0));
				entry.setNotes(getStringValue(logEntryNode, "notes"));
				entry.setDiagram(getBinaryValue(logEntryNode, "diagram"));
				entry.setLastModifiedUtc(lastModifiedDate.equals(new Date(0)) ? new Date(System.currentTimeMillis()) : lastModifiedDate);
				entry.setUniqueId(uniqueId);
				
				// update rigs
				entry.getRigs().clear();
				Element rigsNode = (Element)logEntryNode.getElementsByTagName("rigs").item(0);
				NodeList rigNodes = rigsNode.getElementsByTagName("rig_id");
				for (int j = 0; j < rigNodes.getLength(); j++)
				{
					// get rig
					Element rigNode = (Element)rigNodes.item(j);
					String rigId = rigNode.getTextContent();
					Rig rig = rigMap.get(rigId);
					// add to entry if found
					if (rig != null)
						entry.getRigs().add(rig);
				}
				
				// save log entry
				if (importType == ImportType.New)
				{
					// add log entry
					logEntryId = repository.addLogEntry(entry);
				}
				else if (importType == ImportType.Update)
				{
					// update log entry
					repository.updateLogEntry(entry, false);
				}
			}
			
			// check if signature needs updating
			Date lastSignatureDate = getDateTimeValue(logEntryNode, "last_signature_utc");
	        if (lastSignatureDate != null &&
	        	(entry.getLastSignatureUtc() == null ||
	             lastSignatureDate.compareTo(entry.getLastSignatureUtc()) > 0))
	        {
	        	// update log entry
	        	entry.setLastSignatureUtc(lastSignatureDate);
				repository.updateLogEntry(entry, false);
				
		        // add signature/log entry map
				String signatureId = getStringValue(logEntryNode, "signature_id");
				if (signatureId != null && signatureLogEntryIds.containsKey(signatureId))
				{
					List<Integer> logEntryIds = signatureLogEntryIds.get(signatureId);
					logEntryIds.add(logEntryId);
				}
		    }
		}
		
		// save signatures
		SignatureRepository signatureRepositroy = new SignatureRepository(context);
		for (String signatureId: signatureMap.keySet())
		{
			// if log entries linked to signature, save to db
			if (signatureLogEntryIds.get(signatureId).size() > 0)
			{
				Signature signature = signatureMap.get(signatureId);
				List<Integer> logEntryIds = signatureLogEntryIds.get(signatureId);
				signatureRepositroy.addSignature(signature, logEntryIds);
			}
		}
	}
	
	private String getStringValue(Element parent, String name)
	{
		NodeList nodes = parent.getElementsByTagName(name);
		if (nodes.getLength() <= 0)
			return "";
		
		return nodes.item(0).getTextContent();
	}
	
	private Integer getIntegerValue(Element parent, String name, Integer defaultValue)
	{
		NodeList nodes = parent.getElementsByTagName(name);
		if (nodes.getLength() <= 0)
			return defaultValue;
		
		String value = nodes.item(0).getTextContent();
		if (value == null || value.length() <= 0)
			return defaultValue;
		return Integer.valueOf(value);
	}
	
	private Integer getIntegerAttributeValue(Element parent, String attributeName, Integer defaultValue)
	{
		String value = parent.getAttribute(attributeName);
		if (value == null || value.length() <= 0)
			return defaultValue;
		return Integer.valueOf(value);
	}
	
	private boolean getBooleanValue(Element parent, String name, boolean defaultValue)
	{
		NodeList nodes = parent.getElementsByTagName(name);
		if (nodes.getLength() <= 0)
			return defaultValue;
		
		String value = nodes.item(0).getTextContent();
		if (value == null || value.length() <= 0)
			return defaultValue;
		
		try
		{
			// if int, compare to 1
			int intVal = Integer.parseInt(value);
			return (intVal == 1);
		}
		catch (Exception ex)
		{
			// otherwise try parsing as true/false
			return Boolean.valueOf(value);
		}
	}
	
	private Date getDateValue(Element parent, String name)
	{
		NodeList nodes = parent.getElementsByTagName(name);
		if (nodes.getLength() <= 0)
			return null;
		
		String value = nodes.item(0).getTextContent();
		try
		{
			return dateFormat.parse(value);
		}
		catch (ParseException e)
		{
			return null;
		}
	}
	
	private Date getDateTimeValue(Element parent, String name)
	{
		NodeList nodes = parent.getElementsByTagName(name);
		if (nodes.getLength() <= 0)
			return new Date(0);
		
		String value = nodes.item(0).getTextContent();
		try
		{
			return dateTimeFormat.parse(value);
		}
		catch (ParseException e)
		{
			return new Date(0);
		}
	}
	
	private byte[] getBinaryValue(Element parent, String name)
	{
		NodeList nodes = parent.getElementsByTagName(name);
		if (nodes.getLength() <= 0)
			return null;
		
		String value = nodes.item(0).getTextContent();
		return Base64.decode(value, Base64.DEFAULT);
	}
	
	private Element findElement(Element parent, String name)
	{
		NodeList children = parent.getChildNodes();
		for (int i = 0; i < children.getLength(); i++)
		{
			Node child = children.item(i);
			if (child instanceof Element && name.equals(child.getNodeName()))
				return (Element)child;
		}
		return null;
	}
}
